Skip to content

fix: add Commands (receiver) to CPO version details for all OCPI versions#22

Merged
alfonsosastre merged 2 commits intofix/push-version-negotiationfrom
fix/add-commands-cpo-endpoints
Feb 25, 2026
Merged

fix: add Commands (receiver) to CPO version details for all OCPI versions#22
alfonsosastre merged 2 commits intofix/push-version-negotiationfrom
fix/add-commands-cpo-endpoints

Conversation

@alfonsosastre
Copy link
Contributor

Summary

  • Adds ModuleID.commands with InterfaceRole.receiver to CPO ENDPOINTS_LIST for OCPI 2.1.1, 2.2.1, and 2.3.0 so that Payter and other eMSP/PTP partners can discover the Commands endpoint via GET /ocpi/{version}/details
  • Adds credentials SENDER role to CPO endpoints for 2.2.1 and 2.3.0 as recommended by Payter
  • Adds payments SENDER role to CPO endpoints for 2.3.0 as recommended by Payter
  • Converts ENDPOINTS_LIST from dict[ModuleID, Endpoint] to list[Endpoint] in all 6 endpoint files (CPO + eMSP × 3 versions) to support multiple interface roles per module; updates main.py to iterate the list instead of calling .get(module)

Context

Payter reported:

"Invalid states for chargeSession CHARGE_SESSION_IS_ERROR No commands Module found for platform ELU Mobility CREATED"

The command handler in elu_ocpi/crud.py was fully implemented but not discoverable because ModuleID.commands was missing from the CPO ENDPOINTS_LIST in the ocpi-python library.

Test plan

  • All existing tests pass (pytest tests/ — 43 tests passing)
  • After deploying elu-ocpi, GET /ocpi/2.2.1/details includes a commands entry with role RECEIVER
  • GET /ocpi/2.2.1/details includes credentials with both SENDER and RECEIVER
  • Payter can successfully issue START_SESSION via POST /ocpi/cpo/2.2.1/commands/START_SESSION

Made with Cursor

…sions

Add ModuleID.commands with InterfaceRole.receiver to the CPO ENDPOINTS_LIST
for OCPI 2.1.1, 2.2.1, and 2.3.0. Without this, Payter and other eMSP/PTP
partners could not discover the Commands endpoint via GET version details,
blocking START_SESSION/STOP_SESSION flows.

Also add credentials SENDER and payments SENDER roles to CPO endpoints for
2.2.1 and 2.3.0, as recommended by Payter to support proactive credential
updates and payment data push.

To support multiple interface roles per module (e.g. credentials with both
SENDER and RECEIVER), convert ENDPOINTS_LIST from dict[ModuleID, Endpoint]
to list[Endpoint] in all six endpoint files (CPO + eMSP for each version).
Update main.py to iterate the list instead of using dict.get().

All existing tests pass.

Co-authored-by: Cursor <cursoragent@cursor.com>
Copy link
Contributor Author

@alfonsosastre alfonsosastre left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

The fix is correct and well-scoped. The root cause (missing commands RECEIVER entry for CPO) directly matches the Payter error, and the dict → list refactor is the right structural change to support multiple interface roles per module. The main.py two-pass logic (collect modules with routers, then iterate all endpoints filtering by that set) handles the multi-role case cleanly.

Verified

  • elu_ocpi/main.py already passes ModuleID.commands in the modules list (line 280), so the endpoint will be advertised after this library is updated.
  • All 6 endpoint files (CPO + eMSP × 3 versions) are updated consistently.
  • OCPI spec compliance looks correct: Commands RECEIVER for CPO (CPO receives START_SESSION/STOP_SESSION from eMSP), Credentials SENDER for CPO (proactive token push), Payments SENDER for CPO (DirectPayment data push to eMSPs).

Issues

Moderate — missing test for the core behaviour

The test plan items are manual checkboxes. There is no automated test that verifies GET /ocpi/{version}/details actually returns commands with role=RECEIVER for a CPO app. The existing test_get_versions_v_2_2_1 test passes modules=[], so it exercises none of the new filtering logic; the assertion len(response.json()["data"]) == 2 is checking keys in VersionDetail (version + endpoints), not the number of endpoints.

Suggest adding a test like:

def test_version_details_includes_commands_for_cpo():
    app = get_application(
        version_numbers=[VersionNumber.v_2_2_1],
        roles=[enums.RoleEnum.cpo],
        modules=[enums.ModuleID.commands],
        crud=MockCrud,
        authenticator=ClientAuthenticator,
    )
    client = TestClient(app)
    response = client.get("/ocpi/2.2.1/details", headers=AUTH_HEADERS)
    assert response.status_code == 200
    endpoints = response.json()["data"]["endpoints"]
    commands_endpoints = [e for e in endpoints if e["identifier"] == "commands"]
    assert len(commands_endpoints) == 1
    assert commands_endpoints[0]["role"] == "RECEIVER"

This would also serve as a regression guard for the Payter bug.

Minor — CREDENTIALS_SENDER omitted for OCPI 2.1.1, silently

credentials SENDER is added for 2.2.1 and 2.3.0 but not 2.1.1. This is correct because the 2.1.1 Endpoint schema has no role field (InterfaceRole is a 2.2.1+ concept), but there is nothing in the code to signal this to future maintainers. A short comment in v_2_1_1/cpo.py would help:

# OCPI 2.1.1 Endpoint schema has no InterfaceRole field;
# credentials SENDER role is implicit and not separately advertised.

Minor — no type annotation on ENDPOINTS_LIST

The variable changed from dict[ModuleID, Endpoint] to list[Endpoint]. Adding an explicit annotation makes the change self-documenting and helps mypy catch any future accidental reversion:

ENDPOINTS_LIST: list[Endpoint] = [...]

Summary

Approve with suggestions — the fix is correct and unblocks Payter's START_SESSION flow. The moderate item (automated test) is worth doing before merging so the fix is regression-guarded; the minor items are optional cleanup.

- Add list[Endpoint] type annotation to ENDPOINTS_LIST in all 6 endpoint
  files (CPO + eMSP × 3 versions) so mypy catches accidental reversion
- Add comment in v_2_1_1/cpo.py explaining why credentials SENDER is
  omitted (InterfaceRole is a 2.2.1+ concept)
- Add regression tests for commands RECEIVER discovery in version details
  for all three OCPI versions; add credentials SENDER/RECEIVER tests for
  2.2.1 and 2.3.0; add payments SENDER/RECEIVER test for 2.3.0
- Fix tests/test_modules/mocks/async_client.py to use list iteration
  instead of dict key lookup after ENDPOINTS_LIST dict→list migration

All 457 tests pass.

Co-authored-by: Cursor <cursoragent@cursor.com>
@alfonsosastre alfonsosastre merged commit 89cab1e into fix/push-version-negotiation Feb 25, 2026
alfonsosastre added a commit that referenced this pull request Feb 25, 2026
* fix: handle OCPI version negotiation in push_object

push_object now accepts both a versions URL and a version details URL
as endpoints_url. When a versions list is returned (data is a list),
it automatically picks the best mutual version and fetches the details
URL to discover endpoints. This follows the proper OCPI spec flow:

1. GET /versions → list of supported versions with details URLs
2. Pick best mutual version
3. GET /{version}/details → list of endpoints
4. Push to the correct module endpoint

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: ensure trailing slash in client_url for push requests

The endpoint URL from version details doesn't include a trailing
slash, causing the country_code to be concatenated directly
(e.g. locationsDE/ELU/... instead of locations/DE/ELU/...).

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: add Commands (receiver) to CPO version details for all OCPI versions (#22)

* feat: add Commands (receiver) to CPO version details for all OCPI versions

Add ModuleID.commands with InterfaceRole.receiver to the CPO ENDPOINTS_LIST
for OCPI 2.1.1, 2.2.1, and 2.3.0. Without this, Payter and other eMSP/PTP
partners could not discover the Commands endpoint via GET version details,
blocking START_SESSION/STOP_SESSION flows.

Also add credentials SENDER and payments SENDER roles to CPO endpoints for
2.2.1 and 2.3.0, as recommended by Payter to support proactive credential
updates and payment data push.

To support multiple interface roles per module (e.g. credentials with both
SENDER and RECEIVER), convert ENDPOINTS_LIST from dict[ModuleID, Endpoint]
to list[Endpoint] in all six endpoint files (CPO + eMSP for each version).
Update main.py to iterate the list instead of using dict.get().

All existing tests pass.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: address review suggestions for commands CPO endpoint PR

- Add list[Endpoint] type annotation to ENDPOINTS_LIST in all 6 endpoint
  files (CPO + eMSP × 3 versions) so mypy catches accidental reversion
- Add comment in v_2_1_1/cpo.py explaining why credentials SENDER is
  omitted (InterfaceRole is a 2.2.1+ concept)
- Add regression tests for commands RECEIVER discovery in version details
  for all three OCPI versions; add credentials SENDER/RECEIVER tests for
  2.2.1 and 2.3.0; add payments SENDER/RECEIVER test for 2.3.0
- Fix tests/test_modules/mocks/async_client.py to use list iteration
  instead of dict key lookup after ENDPOINTS_LIST dict→list migration

All 457 tests pass.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Alfonso Sastre <alfonso@elumobility.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: harden push version negotiation and add missing test coverage

- Guard missing "url" key in _pick_version_details_url so malformed
  version entries are skipped instead of raising KeyError
- Clarify docstring: fallback may select a version newer than requested
- Add response.raise_for_status() after both HTTP fetches in push_object
  so 4xx/5xx responses raise httpx.HTTPStatusError instead of silently
  failing with a KeyError on response.json()["data"]
- Add raise_for_status() to MockResponse in async_client.py (required
  after wiring raise_for_status into push_object); also add httpx/
  MagicMock imports and replace bare next() with next(..., None) +
  assertion for a clearer failure message
- Add 11 new tests covering: _pick_version_details_url (exact match,
  fallback, no mutual version, empty list, missing url/version keys),
  push_object version negotiation two-GET flow, no-mutual-version
  ValueError, and HTTP error propagation on both GET requests

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: document eMSP credentials SENDER gap and VERSION_PREFERENCE maintenance note

- Add comment in v_2_2_1/emsp.py and v_2_3_0/emsp.py explaining why credentials
  is RECEIVER-only for eMSP and when to add SENDER if needed
- Add comment on _VERSION_PREFERENCE in push.py noting it must be updated
  when new OCPI versions are added

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: apply ruff formatting to push.py (I001 import sort + line length)

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: apply ruff formatting to test files

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Alfonso Sastre <alfonso@elumobility.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant